Terraform 1.9 の新機能紹介
Terraformのversion 1.9が2024年の6月26日にGAになりました。1.9の新機能を見ていきましょう。
変数のvalidationで色々参照できるようになった
変数にはvalidationを実装することができます。例えば以下のようなものです。
variable "aws_account_id" {
type = string
description = "AWS Account ID"
validation {
condition = can(regex("^[0-9]{12}$", var.aws_account_id))
error_message = "Invalid AWS accountID."
}
}
これはAWSアカウントIDが格納されるのを想定した変数です。AWSアカウントIDは必ず12桁の数字ですので、そうでない場合はエラーにしています。
上記例では condition
句にて変数自身(var.aws_account_id
)を参照しています。実はこれまでは参照できる変数はこのようにvalidationされる変数自身のみでした。
1.9から他の要素も参照できるようになりました。
- 他の変数
- data source
- local変数
他の変数を参照する例
例えば、リリースブログに記載されていた例ですと以下のようなコードが考えられます。
variable "create_cluster" {
description = "Whether to create a new cluster."
type = bool
default = false
}
variable "cluster_endpoint" {
description = "Endpoint of the existing cluster to use."
type = string
default = ""
validation {
condition = var.create_cluster == false ? length(var.cluster_endpoint) > 0 : true
error_message = "You must specify a value for cluster_endpoint if create_cluster is false."
}
}
引用元: Terraform 1.9 enhances input variable validations
このコードはおそらくクラスター(EKSクラスター?)をこのモジュール内で新規作成するか、もしくは既存のクラスター(のエンドポイント)を参照するか選べるようになっているモジュールです。
cluster_endpoint
変数は、新規作成する場合は指定不要ですが、既存クラスター参照の場合は必須にしたいですよね。そのような条件が condition = var.create_cluster == false ? length(var.cluster_endpoint) > 0 : true
でうまく実現できています。
data sourceを参照する例
実在するインスタンスタイプのみ変数値として許可する、が実現できます。
data "aws_ec2_instance_types" "test" {}
variable "instance_type" {
type = string
description = "instance type you want to use"
validation {
condition = can(index(data.aws_ec2_instance_types.test.instance_types , var.instance_type))
error_message = "Invalid instance type."
}
}
リソースのattributesも参照できた
まあ使う機会はないと思いますが、以下のようなリソースのattributesを参照するコードもエラーなく実行できました。
resource "aws_s3_bucket" "example" {
bucket_prefix = "my-tf-test-bucket"
}
variable "region" {
type = string
validation {
condition = var.region == aws_s3_bucket.example.region
error_message = "This validation is meaningless"
}
}
この場合、 aws_s3_bucket.exampleリソース作成後にvalidationが実行されました。
% terraform apply -var "region=hoge"
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# aws_s3_bucket.example will be created
+ resource "aws_s3_bucket" "example" {
(割愛)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_s3_bucket.example: Creating...
aws_s3_bucket.example: Creation complete after 1s [id=my-tf-test-bucket20240703055717715700000001]
╷
│ Error: Invalid value for variable
│
│ on variables.tf line 17:
│ 17: variable "region" {
│ ├────────────────
│ │ aws_s3_bucket.example.region is "ap-northeast-1"
│ │ var.region is "hoge"
│
│ This validation is meaningless
│
│ This was checked by the validation rule at variables.tf:20,3-13.
参照しているaws_s3_bucket.example
が作成済の2回目以降のapplyでは、planフェーズでvalidationが行われました。
% terraform apply -var "region=hoge"
aws_s3_bucket.example: Refreshing state... [id=my-tf-test-bucket20240703055717715700000001]
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Error: Invalid value for variable
│
│ on variables.tf line 17:
│ 17: variable "region" {
│ ├────────────────
│ │ aws_s3_bucket.example.region is "ap-northeast-1"
│ │ var.region is "hoge"
│
│ This validation is meaningless
│
│ This was checked by the validation rule at variables.tf:20,3-13.
templatestring関数
既存のtemplatefile関数の亜種みたいな関数です。
templatefileはローカルにあるファイルをベースに一部記述を動的に変更できる関数です。
対して今回追加されたtemplatestring関数は、ファイルを用意する必要なく、変数やresource attributeなどをベースに一部記述を動的に変更できます。
Terraform公式ブログで紹介されていたの例は、インターネット越しにテンプレートを取得してそれを加工する例です。
data "http" "manifest" {
url = "https://git.democorp.example/repocontent/k8s-templates/ingress-template.yaml"
}
locals {
manifest_final = templatestring(data.http.manifest.response_body, {
APP_NAME = var.app_name
NAMESPACE = kubernetes_namespace_v1.example.metadata.0.name
SERVICE_NAME = kubernetes_service_v1.example.metadata.0.name
CONTAINER_PORT = var.container_port
})
}
resource "kubernetes_manifest" "example" {
manifest = local.manifest_final
}
引用元: Terraform 1.9 enhances input variable validations
他にも、テンプレートを外部ファイル化するまでもないなと思った場合に、ヒアドキュメント形式でローカル変数を用意して使う事も考えられますね。以下が例です。
以下ブログではtemplatefile関数を使ってIAM Policyを定義しています。
これをtemplatestring関数で書き換えると以下になります。
locals {
policy_template = <<-EOT
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "arn:aws:dynamodb:$${region}:$${account_id}:table/$${table_name}"
}]
}
EOT
}
resource "aws_iam_policy" "templatestring" {
name = "templatestring"
policy = templatestring(
local.policy_template,
{
region = data.aws_region.current.id,
account_id = data.aws_caller_identity.current.account_id,
table_name = aws_dynamodb_table.book.name
}
)
}
注意点としては、 local.policy_template
の中の変数は一旦$${hoge}
と$
を2個書いてエスケープさせることです。これをしておかないとtemplatestring関数実行前の、local変数評価時に変数部分が変換されてしまうので。
余談ですが、同様のことはこれまでも replace関数で実現できましたね。少しだけtemplatestring関数の方が読みやすいですかね?
moved blockで null_resource → terraform_dataリソースの移動ができる
前提情報
- null providerの
null_resource
という、なんというか「かゆいところに手が届く感じ」のリソースがあります。AWS providerなどの各通常リソースだけでは実現が難しい要件があった場合に、特定条件下で任意のコマンドを実行する、みたいなことが実現できます。ただ、可読性が落ちる場合が多いので多用は厳禁、だと私は思っています。 - version 1.4にてTerraform本体に
null_resource
と同等のterraform_data
リソースが追加されました。これにより、既存のnull_resource
はterraform_data
リソースに置き換えていくことが推奨されました。 - version 1.8にて、異なるリソース間でmoved blockを使ってリソース移動ができるようになりました。ただしリソース移動できるのは、各プロバイダーがサポートしているリソース間のみです。
本題
本version 1.9より、moved blockで null_resource
→ terraform_data
リソースの移動ができるようになりました。
以下は、TypeScriptのLambda関数をTerraformだけでデプロイする | DevelopersIOで使用したnull_resource
をmoved blockを使ってterraform_data
リソースに移動させた例です。以下だけで簡単に移動できました!
- moved blockを書く
- resourceタイプを null_resource → terraform_dataに変更
triggers
argumentをtriggers_replace
に変更- (他のリソースにて
null_resource.lambda_build
をdepends_on
で参照していたので、それをterraform_data.lambda_build
に変更)
+ moved {
+ from = null_resource.lambda_build
+ to = terraform_data.lambda_build
+ }
+
+ resource "terraform_data" "lambda_build" {
- resource "null_resource" "lambda_build" {
depends_on = [aws_s3_bucket.lambda_assets]
+ triggers_replace = {
- triggers = {
code_diff = join("", [
for file in fileset(local.helloworld_function_dir_local_path, "{*.ts, package*.json}")
: filebase64("${local.helloworld_function_dir_local_path}/${file}")
])
}
provisioner "local-exec" {
command = "cd ${local.helloworld_function_dir_local_path} && npm install"
}
provisioner "local-exec" {
command = "cd ${local.helloworld_function_dir_local_path} && npm run build"
}
provisioner "local-exec" {
command = "aws s3 cp ${local.helloworld_function_package_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.helloworld_function_package_s3_key}"
}
provisioner "local-exec" {
command = "openssl dgst -sha256 -binary ${local.helloworld_function_package_local_path} | openssl enc -base64 | tr -d \"\n\" > ${local.helloworld_function_package_base64sha256_local_path}"
}
provisioner "local-exec" {
command = "aws s3 cp ${local.helloworld_function_package_base64sha256_local_path} s3://${aws_s3_bucket.lambda_assets.bucket}/${local.helloworld_function_package_base64sha256_s3_key} --content-type \"text/plain\""
}
}
Terraform will perform the following actions:
# null_resource.lambda_build has moved to terraform_data.lambda_build
resource "terraform_data" "lambda_build" {
id = "2045579073458313029"
# (1 unchanged attribute hidden)
}
Plan: 0 to add, 0 to change, 0 to destroy.
これは個人的にはとても嬉しいですねー。というのもまだnull_resource
のterraform_data
リソース移行をやっていない箇所があったので。本機能を使ってスマートに移行させていきたいと思います!
removed blockでprovisionerを定義できる
removed blockはversion 1.7で追加された機能です。削除されたリソースの情報をコード内に残しておくことができます。
lifecycle
ブロックのdestroy
値を falseにした場合、リソースはTerrafrom管理外になるだけで削除はされません。
今回の新機能は、destroy
値をtrueにした場合に、provisionerを定義することができるようになった、というものです。これによってリソース削除時に任意の処理を実行できます。以下は削除するEC2インスタンスのidを出力するprovisionerの例です。
removed {
from = aws_instance.example
lifecycle {
destroy = true
}
provisioner "local-exec" {
when = destroy
command = "echo 'Instance ${self.id} has been destroyed.'"
}
}
引用元: Removing Resources | Resources - Configuration Language | Terraform | HashiCorp Developer
% terraform apply
data.aws_ami.ubuntu: Reading...
aws_instance.example: Refreshing state... [id=i-04ed96fbb7922abef]
data.aws_ami.ubuntu: Read complete after 0s [id=ami-0162fe8bfebb6ea16]
Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# aws_instance.example will be destroyed
# (because aws_instance.example is not in configuration)
- resource "aws_instance" "example" {
(割愛)
}
Plan: 0 to add, 0 to change, 1 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_instance.example: Destroying... [id=i-04ed96fbb7922abef]
aws_instance.example: Provisioning with 'local-exec'...
aws_instance.example (local-exec): Executing: ["/bin/sh" "-c" "echo 'Instance i-04ed96fbb7922abef has been destroyed.'"]
aws_instance.example (local-exec): Instance i-04ed96fbb7922abef has been destroyed.
aws_instance.example: Still destroying... [id=i-04ed96fbb7922abef, 10s elapsed]
aws_instance.example: Still destroying... [id=i-04ed96fbb7922abef, 20s elapsed]
aws_instance.example: Still destroying... [id=i-04ed96fbb7922abef, 30s elapsed]
aws_instance.example: Destruction complete after 31s
Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
上記の実行例を見ていただくとわかりますが、provisionerが実行されるのはdestroy処理の前ですので、その点ご注意ください。つまり、リソース削除の前処理には使えるけど、後処理には使えないですね。
リソースブロックにdestroy時のprovisionerを書くケースとの比較
1.9以前でも以下のように、リソース内にdestroy時のprovisionerを定義することはできました。
resource "aws_instance" "example" {
instance_type = "t3.nano"
ami = data.aws_ami.ubuntu.id
provisioner "local-exec" {
when = destroy
command = "echo 'Instance ${self.id} has been destroyed.'"
}
}
この書き方と、新機能の「removed blockでprovisionerを定義する」の違いは何でしょう?
1. 発動条件が異なる
「removed blockでprovisionerを定義する」の場合、該当リソース(今回の例だとaws_instance.example
)のコードを削除した後のterraform applyでprovisionerが実行されます。
ところが「リソース内にdestroy時のprovisionerを定義する」の場合、このケースだとprovisionerは実行されません。provisionerもリソースコードの中に含まれているので、一緒に削除された、ということになってしまいます。
というわけで、「リソース内にdestroy時のprovisionerを定義する」の場合、以下の様にしてコードの削除とリソースの削除のタイミングをズラす必要があります。
count = 0
をリソースブロックに追加するterraform apply
実行 → リソースは削除され、削除前にprovisionerが実行される- リソースコード全体を削除(当然その中のprovisionerのコードも削除される)
terraform apply
実行
ちょっとトリッキーというか、わかりにくいですよね。
2. provisionerの情報がコードに残る
「removed blockでprovisionerを定義する」の場合、リソース削除後もremoved blockは残る(残していても良い)ので、「以前こういう処理が実行されたんだな」ということが後からでも容易に把握できます。
一方「リソース内にdestroy時のprovisionerを定義する」ですと該当のprovisionerのコードはリソースコードの一部なので削除されているはずです。VCSで履歴を辿るなどしないと過去に実行されたprovisionerの内容はわからないでしょう。まあ、前述の「コードの削除とリソースの削除のタイミングをズラす4ステップ」の3と4をやらなければprovisioner コードを残しておくことはできますが、その場合 count = 0
がついたリソースコードが残っていることになるので、それはそれでわかりにくいコードだと思います。
これら2点の観点から、今後は基本的には「removed blockでprovisionerを定義する」の方を採用するのが良いのではないかと思います。